/*
Copyright (c) 2012, 2013, 2014 Countly

Permission is hereby granted, free of charge, to any person obtaining (a) copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/

package ly.count.ohos.sdk;

import ohos.aafwk.ability.Ability;
import ohos.aafwk.ability.SystemMemoryInfo;
import ohos.app.Context;
import ohos.batterymanager.BatteryInfo;
import ohos.bundle.AbilityInfo;
import ohos.data.usage.StatVfs;
import ohos.media.audio.AudioManager;
import ohos.net.NetHandle;
import ohos.net.NetManager;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * This class provides several static methods to retrieve information about
 * the current device and operating environment for crash reporting purposes.
 *
 * @since 2021-04-13
 */
class CrashDetails {
    static Map<String, Object> customSegments = null;

    private static final int MAX_BREAD_CRUMB_LIMIT = 1000; // the limit of how many breadcrumbs can be saved
    private static final int MAX_BREAD_CRUMB_SIZE = 1000; // maximum allowed length of (a) breadcrumb in characters
    private static final int SYSTEM_VALUE = 1024;
    private static final LinkedList<String> logs = new LinkedList<>();
    private static final int START_TIME = UtilsTime.currentTimestampSeconds();
    private static boolean isInBackground = true;
    private static long totalMemory = 0L;

    /**
     * getTotalRAM
     *
     * @return TotalRAM
     */
    private static long getTotalRAM() {
        if (totalMemory == 0) {
            RandomAccessFile reader = null;
            String load;
            try {
                reader = new RandomAccessFile("/proc/meminfo", "r");
                load = reader.readLine();

                // Get the Number value from the string
                Pattern pattern = Pattern.compile("(\\d+)");
                Matcher matcher = pattern.matcher(load);
                String value = "";
                while (matcher.find()) {
                    value = matcher.group(1);
                }
                try {
                    totalMemory = Long.parseLong(value) / SYSTEM_VALUE;
                } catch (NumberFormatException ex) {
                    totalMemory = 0;
                }
            } catch (IOException ex) {
                try {
                    if (reader != null) {
                        reader.close();
                    }
                } catch (IOException exc) {
                    exc.printStackTrace();
                }
                ex.printStackTrace();
            } finally {
                try {
                    if (reader != null) {
                        reader.close();
                    }
                } catch (IOException exc) {
                    exc.printStackTrace();
                }
            }
        }
        return totalMemory;
    }

    /**
     * Notify when app is in foreground
     */
    static void inForeground() {
        isInBackground = false;
    }

    /**
     * Notify when app is in background
     */
    static void inBackground() {
        isInBackground = true;
    }

    /**
     * Returns app background state
     *
     * @return isInBackground
     */
    static String isInBackground() {
        return Boolean.toString(isInBackground);
    }

    /**
     * Adds (a) record in the log
     *
     * @param record
     */
    static void addLog(String record) {
        int recordLength = record.length();
        if (recordLength > MAX_BREAD_CRUMB_SIZE) {
            Countly.sharedInstance().L.d("Breadcrumb exceeds character limit: ["
                + recordLength + "], reducing it to: [" + MAX_BREAD_CRUMB_SIZE + "]");
            record = record.substring(0, Math.min(MAX_BREAD_CRUMB_SIZE, recordLength));
        }

        logs.add(record);

        if (logs.size() > MAX_BREAD_CRUMB_LIMIT) {
            Countly.sharedInstance().L.d("Breadcrumb amount limit exceeded, deleting the oldest one");
            logs.removeFirst();
        }
    }

    /**
     * Returns the collected logs.
     *
     * @return allLogs
     */
    static String getLogs() {
        StringBuilder allLogs = new StringBuilder();

        for (String log : logs) {
            allLogs.append(log).append("\n");
        }
        logs.clear();
        return allLogs.toString();
    }

    /**
     * Adds developer provided custom segments for crash,
     * like versions of dependency libraries.
     *
     * @param segments
     */
    static void setCustomSegments(Map<String, Object> segments) {
        customSegments = new HashMap<>();

        for (Map.Entry<String, Object> pair : segments.entrySet()) {
            String key = pair.getKey();
            Object value = pair.getValue();

            if (value instanceof Integer) {
                customSegments.put(key, (Integer) value);
            } else if (value instanceof Double) {
                customSegments.put(key, (Double) value);
            } else if (value instanceof String) {
                customSegments.put(key, (String) value);
            } else if (value instanceof Boolean) {
                customSegments.put(key, (Boolean) value);
            } else {
                Countly.sharedInstance().L.w("Unsupported data type added as custom crash segment");
            }
        }

        customSegments.putAll(segments);
    }

    /**
     * Get custom segments json string
     *
     * @param additionalCustomSegmentation
     * @return JSONObject
     */
    static JSONObject getCustomSegmentsJson(final Map<String, Object> additionalCustomSegmentation) {
        if (additionalCustomSegmentation == null && (customSegments == null || customSegments.isEmpty())) {
            return null;
        }

        JSONObject returnedSegmentation = new JSONObject();

        if (customSegments != null) {
            for (String key : customSegments.keySet()) {
                if (key != null) {
                    try {
                        returnedSegmentation.put(key, customSegments.get(key));
                    } catch (JSONException e) {
                        Countly.sharedInstance().L.w("[getCustomSegmentsJson] Failed to add custom segmentation to crash");
                    }
                }
            }
        }

        if (additionalCustomSegmentation != null) {
            for (String k : additionalCustomSegmentation.keySet()) {
                if (k != null) {
                    try {
                        returnedSegmentation.put(k, additionalCustomSegmentation.get(k));
                    } catch (JSONException e) {
                        Countly.sharedInstance().L.w("[getCustomSegmentsJson] Failed to add custom segmentation to crash");
                    }
                }
            }
        }

        return returnedSegmentation;
    }

    /**
     * Returns the current device manufacturer.
     *
     * @return Manufacturer
     */
    @SuppressWarnings("SameReturnValue")
    static String getManufacturer() {
        return PropHelper.getInstance().getProp("ro.product.manufacturer");// [ro.product.manufacturer]: [HUAWEI]
    }

    /**
     * Returns the current device cpu.
     *
     * @return Cpu
     */

    static String getCpu() {
        return PropHelper.getInstance().getProp("ro.product.cpu.abi");// [ro.product.cpu.abi]: [arm64-v8a]
    }

    /**
     * Returns the current device openGL version.
     *
     * @param context
     * @return version
     */
    static String getOpenGL(Context context) {
        int deviceGLESVersion = context.getAbilityManager().getDeviceConfigInfo().getDeviceGLESVersion();
        int version = (deviceGLESVersion & 0xffff0000) >> 16;
        return Integer.toString(version);
    }

    /**
     * Returns the current device RAM amount.
     *
     * @param context
     * @return current device RAM amount
     */
    static String getRamCurrent(Context context) {
        SystemMemoryInfo systemMemoryInfo = new SystemMemoryInfo();
        context.getAbilityManager().getSystemMemoryInfo(systemMemoryInfo);
        long availSysMem = systemMemoryInfo.getAvailSysMem() / 1024 / 1024;
        long totalSysMem = systemMemoryInfo.getTotalSysMem() / 1024 / 1024;
        long usage = totalSysMem - availSysMem;
        return Long.toString(usage);
    }

    /**
     * Returns the total device RAM amount.
     *
     * @param context
     * @return the total device RAM amount
     */
    static String getRamTotal(Context context) {
        SystemMemoryInfo systemMemoryInfo = new SystemMemoryInfo();
        context.getAbilityManager().getSystemMemoryInfo(systemMemoryInfo);
        long totalSysMem = systemMemoryInfo.getTotalSysMem() / 1024 / 1024;
        return Long.toString(totalSysMem);
    }

    /**
     * Returns the current device disk space.
     *
     * @param context
     * @return the current device disk space
     */

    static String getDiskCurrent(Context context) {
        StatVfs statVfs = new StatVfs(context.getExternalCacheDir().getAbsolutePath());
        long available = statVfs.getAvailableSpace() / 1024 / 1024;
        return Long.toString(available);
    }

    /**
     * Returns the current device disk space.
     *
     * @param context
     * @return the current device disk space
     */
    static String getDiskTotal(Context context) {
        StatVfs statVfs = new StatVfs(context.getExternalCacheDir().getAbsolutePath());
        long total = statVfs.getSpace() / 1024 / 1024;
        return Long.toString(total);
    }

    /**
     * Returns the current device battery level.
     *
     * @param context
     * @return the current device battery level
     */
    static String getBatteryLevel(Context context) {
        BatteryInfo batteryInfo = new BatteryInfo();
        int capacity = batteryInfo.getCapacity();
        return Integer.toString(capacity);
    }

    /**
     * Get app's running time before crashing.
     *
     * @return app's running time before crashing
     */
    static String getRunningTime() {
        return Integer.toString(UtilsTime.currentTimestampSeconds() - START_TIME);
    }

    /**
     * Returns the current device orientation.
     *
     * @param ability
     * @return the current device orientation
     */
    static String getOrientation(Ability ability) {
        AbilityInfo.DisplayOrientation orientation = ability.getAbilityInfo().getOrientation();
        if (orientation == AbilityInfo.DisplayOrientation.LANDSCAPE) {
            return "Landscape";
        } else if (orientation == AbilityInfo.DisplayOrientation.PORTRAIT) {
            return "Portrait";
        } else if (orientation == AbilityInfo.DisplayOrientation.UNSPECIFIED) {
            return "Unknown";
        }
        return "Unknown";

    }

    /**
     * Checks if device is rooted.
     *
     * @return isRooted
     */
    static String isRooted() {
        String[] paths = {
                "/sbin/su", "/system/bin/su", "/system/xbin/su", "/data/local/xbin/su", "/data/local/bin/su", "/system/sd/xbin/su",
                "/system/bin/failsafe/su", "/data/local/su"
        };
        for (String path : paths) {
            if (new File(path).exists()) return "true";
        }
        return "false";
    }

    /**
     * Checks if device is online.
     *
     * @param context
     * @return isOnline
     */
    static String isOnline(Context context) {
        try {
            NetManager netManager = NetManager.getInstance(context);
            NetHandle[] allNets = netManager.getAllNets();
            if (allNets != null && allNets.length > 0) {
                return "true";
            }
            return "false";
        } catch (Exception e) {
            Countly.sharedInstance().L.w("Got exception determining connectivity", e);
        }
        return null;
    }

    /**
     * Checks if device is muted.
     *
     * @param context
     * @return isMuted
     */
    static String isMuted(Context context) {
        try {
            AudioManager audio = new AudioManager(context);
            return audio.isMute(AudioManager.AudioVolumeType.STREAM_RING) ? "true" : "false";
        } catch (Throwable thr) {
            return "false";
        }
    }

    /**
     * Returns a URL-encoded JSON string containing the device crash report
     * See the following link for more info:
     * http://resources.count.ly/v1.0/docs/i
     *
     * @param context
     * @param ability
     * @param error
     * @param nonfatal
     * @param isNativeCrash
     * @param additionalCustomSegmentation
     * @return CrashData
     */
    static String getCrashData(final Context context, Ability ability, String error, Boolean nonfatal, boolean isNativeCrash, final Map<String, Object> additionalCustomSegmentation) {
        final JSONObject json = new JSONObject();

        fillJSONIfValuesNotEmpty(json,
                "_error", error,
                "_nonfatal", Boolean.toString(nonfatal),
                "_device", DeviceInfo.getDevice(),
                "_os", DeviceInfo.getOS(),
                "_os_version", DeviceInfo.getOSVersion(),
                "_resolution", DeviceInfo.getResolution(context),
                "_app_version", DeviceInfo.getAppVersion(context),
                "_manufacture", getManufacturer(),
                "_cpu", getCpu(),
                "_opengl", getOpenGL(context),
                "_root", isRooted(),
                "_ram_total", getRamTotal(context),
                "_disk_total", getDiskTotal(context)
        );

        if (!isNativeCrash) {
            //if is not a native crash
            fillJSONIfValuesNotEmpty(json,
                    "_logs", getLogs(),
                    "_ram_current", getRamCurrent(context),
                    "_disk_current", getDiskCurrent(context),
                    "_bat", getBatteryLevel(context),
                    "_run", getRunningTime(),
                    "_orientation", getOrientation(ability),
                    "_online", isOnline(context),
                    "_muted", isMuted(context),
                    "_background", isInBackground()
            );
        } else {
            //if is a native crash
            try {
                json.put("_native_cpp", true);
            } catch (JSONException ignored) {
            }
        }

        try {
            json.put("_custom", getCustomSegmentsJson(additionalCustomSegmentation));
        } catch (JSONException e) {
            //no custom segments
        }
        return json.toString();
    }

    /**
     * Utility method to fill JSONObject with supplied objects for supplied keys.
     * Fills json only with non-null and non-empty key/value pairs.
     *
     * @param json    JSONObject to fill
     * @param objects varargs of this kind: key1, value1, key2, value2, ...
     */
    static void fillJSONIfValuesNotEmpty(final JSONObject json, final String... objects) {
        try {
            if (objects.length > 0 && objects.length % 2 == 0) {
                for (int i = 0; i < objects.length; i += 2) {
                    final String key = objects[i];
                    final String value = objects[i + 1];
                    if (value != null && value.length() > 0) {
                        json.put(key, value);
                    }
                }
            }
        } catch (JSONException ignored) {
            // shouldn't ever happen when putting String objects into a JSONObject,
            // it can only happen when putting NaN or INFINITE doubles or floats into it
        }
    }
}
